We've learned how to display data, and pass data as props. What we haven't learned is how to manipulate data! This is where state comes in.
xxxxxxxxxx
111const handleAction = someEvent => {
2 console.log("Some event : ", someEvent);
3};
4
5const Child = props => {
6 return <button onClick={props.onAction}> Click Me! </button>;
7};
8
9export const Parent = () => {
10 return <Child />;
11};
Suppose we wanted to start counting upward when clicking the child's button. There are two ways that you'll see as an approach to solving this problem:
We're not going to spend too much time on classes, but since they're not deprecated, we're definitely going to talk about them for a brief moment. For reference to what's happening, the following code is from ParentClassComponent.jsx
in the example-state
project:
xxxxxxxxxx
521import React from "react";
2import { Child } from "../Child";
3
4
5// Instead of defining our component functionally like we've done before,
6// we define our component as a class extending `React.Component`
7export class ParentComponent extends React.Component {
8
9 // We need a constructor to build our component (which takes in our props)
10 constructor(props) {
11 super(props); // and then call our superconstructor.
12
13 // We assign our state variables by using `this.state`.
14 // Any key within state's object will be accessible
15 this.state = {
16 timesPressed: 0
17 };
18
19 // When we want to have functions access state, we need to bind them like so
20 this.handleAction = this.handleAction.bind(this);
21 }
22
23
24 // Here is a handle action function that we'll send to our child component.
25 handleAction(action) {
26 console.log("Handling the action! ", action);
27
28 // To manipulate state, we can use `this.setState`,
29 // and pass in a new updated state object
30 // (here, we're adding one to our timesPressed)
31 this.setState({
32 timesPressed: this.state.timesPressed + 1
33 });
34
35 console.log("TIMES PRESSED? ", this.state.timesPressed);
36 }
37
38
39 // And below, we need a way to work with our JSX, so we use the `render` function.
40 // You can put anything inside this function (i.e. anything before the return),
41 // it just needs to return JSX at the end.
42 render() {
43
44 return (
45 <>
46 <Child onClick={this.handleAction} />
47 <div>That dang button got clicked {this.state.timesPressed} times </div>
48 </>
49 );
50 }
51}
52
Regardless of whether or not you use multiple of the same component, they will each have their own state. Try replacing <ParentComponent />
in the ReactDOM.render
with <ParentChildParty />
in the index.js
file.
xxxxxxxxxx
241//... above code removed
2
3const ParentChildParty = () => {
4 return (
5 <>
6 <h1>Parent 1</h1>
7 <ParentComponent />
8 <br />
9 <h1>Parent 2</h1>
10 <ParentComponent />
11 <br />
12 <h1>Parent 3</h1>
13 <ParentComponent />
14 <br />
15 <div>
16 Clearly, every single instance of "ParentClassComponent" has its own
17 state. Try opening the devtools and looking around
18 </div>
19 </>
20 );
21};
22
23ReactDOM.render(<ParentComponent />, document.querySelector("#root"));
24
Each ParentComponent
has its own state that is accessed individually!
Asynchronicity, not a word, but it sounds cool, and setState
is an asynchronous function. Let's try adding a console log to read from state immediately after we set it:
xxxxxxxxxx
91
2 handleAction(action) {
3 this.setState({
4 timesPressed: this.state.timesPressed + 1
5 });
6
7 console.log("TIMES PRESSED? ", this.state.timesPressed);
8 }
9
What ends up happening is that we wind up getting a console log in our browser that's logging the previous state, while our browser itself is rendering what the actual state is. For example, suppose this.state.timesPressed
was 2
, if we called handAction
, we'll then asynchronously call setState
, and then move onto console.log
, where 2
will get printed to the console while state is being updated to 3!
To keep your code clean, you can just as easily pass a function to your set state that returns a state object of whatever you wish to update. The function by default takes in (state, props)
. That is, it takes in the previous state and props (prior to being updated). You can see an example of this in ParentClassWithStateFunction.jsx
in the corresponding code.
xxxxxxxxxx
131
2 addOneToState(state, props) {
3 console.log("State?", state);
4 console.log("Props???", props);
5 return {
6 timesPressed: this.state.timesPressed + 1
7 };
8 }
9
10 handleAction(action) {
11 this.setState(this.addOneToState);
12 }
13
More State Functions
You can just as easily create other methods that work on the same state variables, that is, it's somewhat pointless having only a single function that works on a state variable. You'll often want to have multiple functions referencing the same state variables to tie your app together, such as a reset function for our button counter (an example can be seen in the ParentClassWithResetFunction.jsx
file:
xxxxxxxxxx
421
2 constructor(props) {
3 super(props);
4 this.state = {
5 timesPressed: 0
6 };
7
8 this.handleAction = this.handleAction.bind(this);
9
10 // MAKE SURE TO BIND YOUR ADDITIONAL FUNCTIONS!
11 // This will constantly turn around and bite you (it does for me)
12 this.resetTimesPressed = this.resetTimesPressed.bind(this);
13 }
14
15 addOneToState() {
16 console.log("inside add1 to state");
17 return {
18 timesPressed: this.state.timesPressed + 1
19 };
20 }
21
22 // added resetStateToZero function
23 // this just sends a `timesPressed: 0` object to state!
24 resetStateToZero() {
25 console.log("inside reset state to 0");
26 return {
27 timesPressed: 0
28 };
29 }
30
31 handleAction(action) {
32 console.log("Handling the action! ", action);
33
34 this.setState(this.addOneToState);
35
36 console.log("TIMES PRESSED? ", this.state.timesPressed);
37 }
38
39 resetTimesPressed(action) {
40 this.setState(this.resetStateToZero);
41 }
42
REITERATING: In order to make sure that you don't find yourself in a pickle (i.e. if you use a class component with constructors), make sure you always bind your functions!
(Try removing a this.<functionName> = this.<functionName>.bind(this)
from the constructor)
What happens if you have more than one element in your state? You likely will want more than one element in your state, such as a text field (example can be seen in ParentClassMultipleStateItems.jsx
):
xxxxxxxxxx
521export class ParentComponent extends React.Component {
2 constructor(props) {
3 console.log("This instatiates the component and brings in props", props);
4 super(props);
5 this.state = {
6 timesPressed: 0,
7 value: ""
8 };
9
10 this.handleAction = this.handleAction.bind(this);
11 this.resetTimesPressed = this.resetTimesPressed.bind(this);
12
13 // BINDING THE NEW FUNCTION THAT WILL HANDLE OUR TYPING IN THE TEXT FIELD
14 this.handleTypeAction = this.handleTypeAction.bind(this);
15 }
16
17 // Removed handleAction/ResetTimesPressed/addOneToState/resetStateToZero for space
18
19 // UPDATING THE VALUE (just like with addOneToState and resetStateToZero)
20 updateTheValue(event) {
21 return {
22 value: event.target.value
23 };
24 }
25
26 // Handling the type action!
27 // Notice that we're passing event into this.updateTheView
28 // which is returning an object, and then being passed into setState?
29 // First class functions rule!
30 handleTypeAction(event) {
31 this.setState(this.updateTheValue(event));
32 }
33
34
35 // Below, we have our text box in our render function!
36 render() {
37 return (
38 <>
39 <Child onClick={this.handleAction} />
40 <div>That dang button got clicked {this.state.timesPressed} times </div>
41 <div>
42 <input
43 type="text"
44 value={this.state.value}
45 onChange={this.handleTypeAction}
46 />
47 </div>
48 <GrandChild onClick={this.resetTimesPressed} />
49 </>
50 );
51 }
52}
When you have more and more complex states, you'll often have compartmentalized data in the form of an object, or an array, etc (may as a user object with a username and password?). When updating your state, I'm sure you've noticed that you only insert the portion of state that you'd like to update. This works great at the top level. That's because state is updated with a shallow copy. If you try to do this with objects in state, it will not deep copy. Take a look at the below code (working components in ParentComponentShallowDeepState.jsx
). When we update username or password, we're not passing in its corresponding value (that is, on username, we're not also passing in password, and vice versa).
Try running the below code. It doesn't necessarily make the most sense for the text boxes (since they don't rerender the values), so we have the username and password displaying within a div after each:
xxxxxxxxxx
761export class ParentComponent extends React.Component {
2 constructor(props) {
3 console.log("This instatiates the component and brings in props", props);
4 super(props);
5 this.state = {
6 timesPressed: 0,
7 value: "",
8 user: {
9 username: "",
10 password: ""
11 }
12 };
13
14 this.handleAction = this.handleAction.bind(this);
15 this.resetTimesPressed = this.resetTimesPressed.bind(this);
16 this.handleUsernameAction = this.handleUsernameAction.bind(this);
17 this.handlePasswordAction = this.handlePasswordAction.bind(this);
18 }
19
20 // handle times pressed/reset with corresponding functionsremoved
21
22 updateUsername(event) {
23 return {
24 user: {
25 username: event.target.value
26 }
27 };
28 }
29
30 updatePassword(event) {
31 return {
32 user: {
33 password: event.target.value
34 }
35 };
36 }
37 handleUsernameAction(event) {
38 this.setState(this.updateUsername(event));
39 console.log("State after setting username", this.state);
40 }
41
42 handlePasswordAction(event) {
43 this.setState(this.updatePassword(event));
44 console.log("State after setting password", this.state);
45 }
46
47 render() {
48 return (
49 <>
50 <Child onClick={this.handleAction} />
51 <div>That dang button got clicked {this.state.timesPressed} times </div>
52 <div>
53 Username:
54 <input
55 type="text"
56 value={this.state.user.username}
57 onChange={this.handleUsernameAction}
58 />
59 <div> Username typed: {this.state.user.username} </div>
60 </div>
61 <br />
62 <br />
63 <div>Password:</div>
64 <input
65 type="password"
66 value={this.state.user.password}
67 onChange={this.handlePasswordAction}
68 />
69 <div> Password typed: {this.state.user.password} </div>
70
71 <br />
72 <GrandChild onClick={this.resetTimesPressed} />
73 </>
74 );
75 }
76}
I'm sure you noticed that the second you started typing in password, the username went away (and the same goes for username). To avoid this problem, you only need to either pass in the part of state you wish to keep:
xxxxxxxxxx
191
2updateUsername(event) {
3 return {
4 user: {
5 username: event.target.value,
6 password: this.state.user.password
7 }
8 };
9}
10
11updatePassword(event) {
12 return {
13 user: {
14 password: event.target.value,
15 username: this.state.user.username
16 }
17 };
18}
19
However, it's pretty likely that you'll have more than just two keys within your object, so you can use the spread operator to get around that and spread whatever object you'd like (in our case, it's this.state.user
):
xxxxxxxxxx
191...
2updateUsername(event) {
3 return {
4 user: {
5 ...this.state.user,
6 username: event.target.value,
7 }
8 };
9}
10
11updatePassword(event) {
12 return {
13 user: {
14 ...this.state.user,
15 password: event.target.value,
16 }
17 };
18}
19...
What's happening above is that we're essentially copying this.state.user
and then overwriting a specific key. CAVEAT: Make sure you spread your object first. If you spread it last, then you'll be overwriting your new data with your old data! Go to ParentComponentShallowDeepState.jsx
to see a working example. Try placing the the spread after your updated values. What do you see?
We've seen that we can update and manipulate our state with the setState methods, but our components are getting incredibly large. Keeping file sizes below a hundred lines is definitely nice to do. Let's clean our code up a little bit. We can start with making use of arrow functions! Because arrow functions don't bind to their own this
and move up a level, they implicitly bind to the class itself (so we don't have to remember all of those this.whatever = this.whatever.bind(this
)).
So now we can remove the constructor entirely, set our state by just creating a state variable and turning our functions into arrow functions (seen in ParentClassClean.jsx
)!
xxxxxxxxxx
881import React from "react";
2import { Child } from "../Child";
3import { GrandChild } from "../GrandChild";
4
5export class ParentComponent extends React.Component {
6 // get rid of that constructor
7 state = {
8 timesPressed: 0,
9 value: "",
10 user: {
11 username: "",
12 password: ""
13 }
14 };
15
16 // change all
17 addOneToState = () => ({
18 timesPressed: this.state.timesPressed + 1
19 });
20
21 resetStateToZero = () => ({
22 timesPressed: 0
23 });
24
25 updateUsername = event => ({
26 user: {
27 username: event.target.value,
28 password: this.state.user.password
29 }
30 });
31
32 updatePassword = event => {
33 return {
34 user: {
35 password: event.target.value,
36 username: this.state.user.username
37 }
38 };
39 }
40
41 handleAction = action => {
42 this.setState(this.addOneToState);
43 };
44
45 resetTimesPressed = action => {
46 this.setState(this.resetStateToZero);
47 };
48
49 handleUsernameAction = event => {
50 this.setState(this.updateUsername(event));
51 console.log("State after setting username", this.state);
52 };
53
54 handlePasswordAction = event => {
55 this.setState(this.updatePassword(event));
56 console.log("State after setting password", this.state);
57 };
58
59 render() {
60 return (
61 <>
62 <Child onClick={this.handleAction} />
63 <div>That dang button got clicked {this.state.timesPressed} times </div>
64 <div>
65 Username:
66 <input
67 type="text"
68 value={this.state.user.username}
69 onChange={this.handleUsernameAction}
70 />
71 <div> Username typed: {this.state.user.username} </div>
72 </div>
73 <br />
74 <br />
75 <div>Password:</div>
76 <input
77 type="password"
78 value={this.state.user.password}
79 onChange={this.handlePasswordAction}
80 />
81 <div> Password typed: {this.state.user.password} </div>
82
83 <br />
84 <GrandChild onClick={this.resetTimesPressed} />
85 </>
86 );
87 }
88}